package com.magenta.geocoding.progorod;

import com.magenta.geocoding.progorod.json.*;
import com.magenta.maxoptra.dto.GeoLocationRecord;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.utils.DateTimeHelper;
import com.magenta.maxoptra.gis.utils.GetRoutePointsByCoordsProfiler;
import com.magenta.maxoptra.gis.utils.LineUnits;
import com.magenta.maxoptra.gis.utils.UnitsConverter;
import com.magenta.mrs.basegeocoding.BaseGeocoding;
import com.magenta.mrs.util.GeoInformationClient;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.codehaus.jackson.map.ObjectMapper;

import java.io.IOException;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.SocketAddress;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.magenta.maxoptra.gis.GeoStatusCode.*;
import static com.magenta.maxoptra.gis.GisParameter.*;

public class GeoProviderProGorod extends BaseGeocoding<String> implements IGeocoding, IReverseGeocoding, IGeoRouting {

    protected java.net.Proxy proxy;


    private static Map<String, GeoStatusCode> progorodErrors = new ConcurrentHashMap<String, GeoStatusCode>();;

    static {
        progorodErrors.put("1", G_GEO_ABORTED_REQUEST); //маршрутизация уже работает. На сервере это означает, что она подвисла, и сервис надо перезапустить.
        progorodErrors.put("2", G_GEO_BAD_FINISH_POINT);   //не установлен финиш
        progorodErrors.put("3", G_GEO_BAD_START_POINT);  //не установлен старт
        progorodErrors.put("7", G_GEO_BAD_START_POINT); //неправильная точка старта
        progorodErrors.put("8", G_GEO_BAD_INTERMEDIATE_POINT);//неправильная промежуточная точка (suberror - номер, не считая старта)
        progorodErrors.put("9", G_GEO_BAD_FINISH_POINT);//неправильная точка финиша
        progorodErrors.put("10",G_GEO_ISOLATED_START_POINT);//изолированная точка старта
        progorodErrors.put("11", G_GEO_ISOLATED_INTERMEDIATE_POINT);//изолированная промежуточная точка (suberror - номер, не считая старта)
        progorodErrors.put("12", G_GEO_ISOLATED_FINISH_POINT );//изолированная точка финиша
        progorodErrors.put("13", G_GEO_UNKNOWN_DIRECTIONS);//несвязный путь
        progorodErrors.put("14", G_GEO_UNKNOWN_DIRECTIONS);//маршрутизация не удалась
    }

    public Map<String, GeoStatusCode> getProviderErrorMap() {
        return progorodErrors;
    }

    public String getProviderName() {
        return MapProviderType.progorod.name();
    }

    @Override
    protected boolean accuracyIsAccurateEnough(String accuracyThreshold, String accuracy) {
        return false;
    }

    @Override
    protected List<GeoAddress> doFindAddressByKeyword(GeoProviderSettings settings, String keyword, int resultAddressLimit, List<String> accuracies) throws GeoException {
        return findAddressByKeyword(settings, keyword, resultAddressLimit);
    }

    @Override
    public void setClient(GeoInformationClient client) {
        this.client = client;
        HttpHost httpProxy = (HttpHost) client.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
        if (httpProxy != null) {
            String proxyHost = httpProxy.getHostName();
            int proxyPort = httpProxy.getPort();
            final Credentials credentials = ((DefaultHttpClient) client).getCredentialsProvider().getCredentials(
                    new AuthScope(proxyHost, proxyPort));

            SocketAddress socketAddress = new InetSocketAddress(proxyHost, proxyPort);
            this.proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, socketAddress);

            Authenticator.setDefault(new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    PasswordAuthentication p = new PasswordAuthentication(credentials.getUserPrincipal().getName(),
                            credentials.getPassword().toCharArray());
                    return p;
                }
            });
        }
    }

    private String doRequest(String url) {
        try {
            String encodedUrl = org.apache.commons.httpclient.util.URIUtil.encodeQuery(url);
            HttpGet get = new HttpGet(encodedUrl);
            get.addHeader("Accept-Language", "ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3");

            HttpResponse response = client.execute(get);
            return EntityUtils.toString(response.getEntity(), "utf-8");
        } catch (Exception e) {
            logger.error(e.getMessage());
        }

        return null;
    }

    public List<GeoAddress> findAddressByKeyword(GeoProviderSettings settings, String keyword, int resultAddressLimit) throws GeoException {
        String forwardGeocodingURL = getForwardGeocodingURL();
        //http://search2.tmcrussia.com/?a=search&q=%s&z=10&n=%s&st=addr
        forwardGeocodingURL = String.format(forwardGeocodingURL, keyword, resultAddressLimit);

        ArrayList<GeoAddress> shortAddresses = new ArrayList<GeoAddress>();
        try {
            String addresses = doRequest(forwardGeocodingURL);
            ObjectMapper mapper = new ObjectMapper();
            SearchResponse response = mapper.readValue(addresses, SearchResponse.class);
            if (response.getSuccess()) {
                List<Res> responses = response.getRes();

                for (Res res : responses) {
                    List<Matches> matches = res.getMatches();
                    if (matches == null) continue;
                    for (Matches match : matches) {
                        GeoAddress address = new GeoAddress();
                        address.setInfo(match.getAddr());
                        address.setLongitude(match.getX());
                        address.setLatitude(match.getY());
                        address.setState(match.getRegion_name());
                        shortAddresses.add(address);
                    }
                }
            }
        } catch (IOException e) {
            throw new GeoException(e);
        }
        return shortAddresses;
    }

    @Override
    public List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings, GeoAddress address, int resultAddressLimit)
            throws GeoException {
        return findAddressByKeyword(settings, address.getInfo(), resultAddressLimit);
    }

    public GeoAddress findBestAddressByKeyword(GeoProviderSettings settings, String keyword) throws GeoException {
        List<GeoAddress> addressByKeyword = findAddressByKeyword(settings, keyword, 1);
        if (addressByKeyword != null && addressByKeyword.size() > 0) {
            return addressByKeyword.get(0);
        }
        return null;
    }

    @Override
    public GeoAddress doReverseGeocoding(GeographicCoordinates geoCoordinates) throws GeoException {
        String reverseGeocodingURL = getReverseGeocodingURL();
        //http://search2.tmcrussia.com/?a=geocode&lon=%s&lat=%s&is_addr=1&r=1000&n=1&at=3
        reverseGeocodingURL = String.format(reverseGeocodingURL, formatDouble(geoCoordinates.getLongitude()), formatDouble(geoCoordinates.getLatitude()));

        GeoAddress address = new GeoAddress();
        try {
            String addresses = doRequest(reverseGeocodingURL);
            ObjectMapper mapper = new ObjectMapper();
            ReverseGeocodeSearchResponse response = mapper.readValue(addresses, ReverseGeocodeSearchResponse.class);
            if (response.getSuccess()) {
                List<ReverceGeocodeRes> responses = response.getRes();
                if (responses!=null && responses.size() > 0) {
                    ReverceGeocodeRes res = responses.get(0);
                    address.setInfo(res.getAddr());
                    address.setLongitude(res.getX());
                    address.setLatitude(res.getY());
                    address.setState(res.getRegion_name());
                }
            }
        } catch (IOException e) {
            throw new GeoException(e);
        }
        return address;
    }

    @Override
    public List<GeoAddress> doReverseGeocodeFull(List<GeographicCoordinates> geoCoordinates) throws GeoException {
        List<GeoAddress> result = new ArrayList<GeoAddress>();
        for (GeographicCoordinates geoCoordinate : geoCoordinates) {
            result.add(doReverseGeocoding(geoCoordinate));
        }
        return result;
    }

    private String getRoutingURL() {
        GisParameters properties = client.getProperties();
        String providerName = getProviderName();
        return properties.get(routing_url.withProvider(providerName));
    }

    private String getForwardGeocodingURL() {
        GisParameters properties = client.getProperties();
        String providerName = getProviderName();
        return properties.get(geocoding_url.withProvider(providerName));
    }

    private String getReverseGeocodingURL() {
        GisParameters properties = client.getProperties();
        String providerName = getProviderName();
        return properties.get(reverse_geocoding_url.withProvider(providerName));
    }

    private boolean useShortestRoute() {
        GisParameters properties = client.getProperties();
        String providerName = getProviderName();
        String property = properties.get(use_shortest_route.withProvider(providerName));
        return property == null || Boolean.parseBoolean(property);
    }

    private boolean useTraffic() {
        GisParameters properties = client.getProperties();
        String providerName = getProviderName();
        String property = properties.get(traffic.withProvider(providerName));
        return property == null || Boolean.parseBoolean(property);
    }

    private static String formatDouble(Double value) {
        DecimalFormat decimalFormat = new DecimalFormat("0.00000");
        DecimalFormatSymbols newSymbols = new DecimalFormatSymbols();
        newSymbols.setDecimalSeparator('.');
        decimalFormat.setDecimalFormatSymbols(newSymbols);
        return decimalFormat.format(value);
    }


    private GeoRoute getRoute_(GeographicCoordinates from, GeographicCoordinates to, List<GeographicCoordinates> coords) throws GeoException {
        String routingURL = getRoutingURL();
        boolean useShortestRoute = useShortestRoute();
        //http://route.tmcrussia.com/cgi/getroute?
        //n=2&p0x=%s&p0y=%s&p1x=%s&p1y=%s&method=%s&type=%s
        routingURL = routingURL + routingPointsString(coords);
        routingURL = routingURL + "&method="+(useShortestRoute ? "shortest" : "optimal");
        routingURL = routingURL + "&type=route";
        boolean traffic = useTraffic();
        if (traffic) routingURL=routingURL+"&traffic=1";
        String addresses = null;
        ProGorodRoute progorodRoute = null;
        try {
            addresses = doRequest(routingURL);
            progorodRoute = new ObjectMapper().readValue(addresses, ProGorodRoute.class);
            if (progorodRoute.getError() == 0) {
                return new GeoRoute(UnitsConverter.convert(LineUnits.METER, LineUnits.KILOMETRE, progorodRoute.getDistance()),
                        progorodRoute.getTime() * DateTimeHelper.MSECS_IN_SECOND, from, to);
            } else {
                GeoStatusCode statusCode = progorodErrors.get(Integer.toString(progorodRoute.getError()));
                if (statusCode == null) {
                    statusCode = GeoStatusCode.G_GEO_UNKOWN_STATUS;
                }
                return new GeoRoute(statusCode.getCode(), -progorodRoute.getError(), from, to);
            }
        } catch (IOException e) {
            logger.error(e);
            throw new GeoException(e);
        }
    }

    @Override
    public GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to) throws GeoException, ServerUnavailableException {
        return getRoute(from, to, null);
    }

    @Override
    public GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to, VehicleType vehicleType) throws GeoException {
        List<GeographicCoordinates> coords = new LinkedList<GeographicCoordinates>();
        coords.add(from);
        coords.add(to);
        return getRoute_(from, to, coords);
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates, VehicleType vehicleType) throws GeoException {
        if (coordinates!=null && coordinates.size() > 1) {
            return getRoute_(coordinates.get(0), coordinates.get(coordinates.size() - 1), coordinates);
        }
        return null;
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoute_ from coordinate to list of coordinates is not supported for GeoProviderProGorod");
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoutes from coordinate to list of coordinates is not supported for GeoProviderProGorod");
    }

    @Override
    public List<GeoRoute> getCoordinateToCoordinateRoutes(List<GeographicCoordinates> coordinates) throws ServerUnavailableException {
        throw new UnsupportedOperationException("getCoordinateToCoordinateRoutes from coordinate to list of coordinates is not supported for " + getProviderName());
    }

    private String routingPointsString(List<GeographicCoordinates> points) {
        String res = "n="+points.size();
        int i = 0;
        for (GeographicCoordinates point : points) {
            res = res + "&p"+i+"x="+formatDouble(point.getLongitude())+"&p"+i+"y="+formatDouble(point.getLatitude());
            i++;
        }
        return res;
    }

    private List<GeoLocationRecord> findRoutePath(List<GeographicCoordinates> coords) throws GeoException {
        GetRoutePointsByCoordsProfiler profiler = new GetRoutePointsByCoordsProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), coords);
        String routingURL = getRoutingURL();
        boolean useShortestRoute = useShortestRoute();
        //http://route.tmcrussia.com/cgi/getroute?
        //n=2&p0x=%s&p0y=%s&p1x=%s&p1y=%s&method=%s&type=%s
        routingURL = routingURL + routingPointsString(coords);
        routingURL = routingURL + "&method="+(useShortestRoute ? "shortest" : "optimal");
        routingURL = routingURL + "&type=route";
        boolean traffic = useTraffic();
        if (traffic) routingURL=routingURL+"&traffic=1";
        List<GeoLocationRecord> path = new ArrayList<GeoLocationRecord>();
        try {
            String addresses = doRequest(routingURL);
            ProGorodRoute progorodRoute = new ObjectMapper().readValue(addresses, ProGorodRoute.class);
            if (progorodRoute.getError() == 0) {
                List<Double[]> points = progorodRoute.getPoints();
                if (points!=null && points.size() > 0) {
                    for (Double[] point : points) {
                        path.add(new GeoLocationRecord(point[0],point[1]));
                    }
                }
                profiler.logDuration("getRoutePoints:resultCount:"+path.size());
                return path;
            } else {
                String errorMessage = "Provider error " + progorodRoute.getError();
                profiler.logError(errorMessage);
                throw new GeoException(errorMessage);
            }
        } catch (IOException e) {
            profiler.logError(e.getMessage());
            logger.error(e);
            throw new GeoException(e);
        }
    }


    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, GeographicCoordinates from, GeographicCoordinates to) throws GeoException {
        List<GeographicCoordinates> coords = new LinkedList<GeographicCoordinates>();
        coords.add(from);
        coords.add(to);
        return findRoutePath(coords);
    }

    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, List<GeographicCoordinates> points) throws GeoException {
        return findRoutePath(points);
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException, ServerUnavailableException {
        return getRoutes(from, to, timeout);
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException {
        return getRoute(from, to, null, timeout);
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates) throws GeoException {
        return getRoute(coordinates, null);
    }


}

